feat(gateway): serve an operator-supplied certificate, and fix the healthcheck - #6859
Conversation
|
Claude finished @geovannewashington's task in 2m 35s —— View job Code Review CompleteReviewed 6 files across code quality, security, testing, language patterns, A few things I verified while reviewing, for the record:
If you push additional changes and want a new review, tag |
d6bc1b5 to
b022337
Compare
|
/review |
|
Claude finished @otavio's task in 1m 42s —— View job Code Review CompleteReviewed 6 files across code quality, security, testing, language patterns, and architecture — no new issues found. The code looks good as-is. A few things verified while reviewing, for the record:
If you push additional changes and want a new review, tag |
The container healthcheck probed http://gateway/healthcheck. That name is the container's own name on the Docker network, never one of the site addresses this proxy serves, so Caddy answered it the way it answers any name it does not recognise: with an empty 200. The probe called the gateway healthy whatever state the stack was in, and a made-up path got exactly the same answer as the real one. With SHELLHUB_AUTO_SSL on it stops being harmless. Automatic HTTPS redirects that name to itself over HTTPS, where no certificate can match it, so the handshake fails and the container never becomes healthy. Every deployment serving TLS has this today. Nothing acts on the status, so it surfaces only as an unhealthy label on a container that plainly works. It now answers on healthcheck.internal, a name of its own, written http:// so automatic HTTPS cannot redirect it. Compose resolves that name to 127.0.0.1 inside the container, so it shares port 80 with the main site without either matching the other's requests. Whether the API answers is left to the API's own healthcheck, which server and ui already have.
b022337 to
e69c3f9
Compare
|
/review |
|
Claude finished @otavio's task in 4m 47s —— View job Code Review CompleteReviewed 6 files across code quality, security, testing, language patterns, and architecture — no new blocking issues found. The code looks good as-is. Verified during review:
Additional notes
If you push additional changes and want a new review, tag |
The gateway had one way to get a certificate: ask a public CA over ACME. That leaves no way to run HTTPS on a name no public authority will sign, which is every internal hostname and every domain a deployment does not own. SHELLHUB_AUTO_SSL could only mean "go and obtain one", and against such a name it fails and keeps retrying for as long as the process runs. SHELLHUB_TLS_CERT_FILE and SHELLHUB_TLS_KEY_FILE name a certificate and its key to serve as they are. Naming them also stops the ACME request, because a site with no tls directive gets Caddy's default, which is to go and ask for one. The paths are read inside the gateway container, so the certificate has to be mounted in. Both or neither, checked even with TLS off: a half-set pair is a typo in every case, and the alternative to refusing it at startup is quietly asking a public CA instead of serving what was supplied.
e69c3f9 to
d2fb447
Compare
What
Two independent changes to the gateway, one commit each.
fix(gateway)makes the container healthcheck answer on a site this proxyactually serves.
feat(gateway)adds SHELLHUB_TLS_CERT_FILE andSHELLHUB_TLS_KEY_FILE, which name a certificate to serve as-is instead of
obtaining one over ACME.
The fix stands alone and could be merged on its own. The feature cannot be
exercised without it, which is why they travel together.
Why
The healthcheck. It probed http://gateway/healthcheck. That name is the
container's own name on the Docker network, never one of the site addresses
the proxy serves, so Caddy answered it the way it answers any name it does
not recognise: with an empty 200. The probe reported healthy whatever state
the stack was in, and a made-up path got exactly the same answer. With
SHELLHUB_AUTO_SSL on it inverts: automatic HTTPS redirects that name to
itself over HTTPS, where no certificate can match it, so the handshake
fails and the container never becomes healthy. Nothing in the compose file
gates on gateway health, so today this surfaces only as an unhealthy label
on a container that plainly works.
The certificate. The gateway had one way to get a certificate: ask a
public CA. That leaves no way to run HTTPS on a name no public authority
will sign, which is every internal hostname and every domain a deployment
does not own. Against such a name SHELLHUB_AUTO_SSL cannot succeed, and
Caddy retries for as long as the process runs. This is what an on-prem
deployment behind a corporate CA needs, and what a local demo needs to
serve a branded domain over trusted HTTPS.
Changes
fix(gateway)
Caddyfile.tmpl: ahttp://healthcheck.internalsite answering/healthz. The scheme is explicit so automatic HTTPS cannot redirect it,and the name is its own so it shares port 80 with the main site without
either matching the other's requests.
docker-compose.yml:extra_hostsresolves that name to127.0.0.1inside the container, and the probe targets it. The name is repeated in
both files, so the two have to move together.
/healthcheckon the real domain still routes tothe API, and no client addresses
healthcheck.internal. It is reachablefrom outside on the published port by sending that
Hostheader, whichreturns the string
okand nothing else.serveranduialready have.feat(gateway)
config.go: the two variables, validatedrequired_witheach other.Both or neither, checked even with TLS off: a half-set pair is a typo in
every case, and the alternative to refusing it at startup is quietly
asking a public CA instead of serving what was supplied.
Caddyfile.tmpl: atls <cert> <key>directive when both are set. Thisis also what keeps Caddy off ACME, since a site with no
tlsdirectivegets the default, which is to go and ask for one. Only the main site is
affected; the web endpoints wildcard is still obtained over DNS-01.
docker-compose.ymlpasses both through;.envand the gateway READMEdocument them. The paths are read inside the gateway container, so the
certificate has to be mounted in.
No behaviour changes when the variables are unset, which is every existing
deployment.
Testing
Beyond the unit tests, this was exercised end to end with a certificate no
public CA would ever sign, on the release gateway image and on this build.
Issue one and trust it locally:
Point the name at the host: add it to
/etc/hostsas127.0.0.1.Bring the stack up with the certificate mounted in, via a compose
override that bind-mounts the pair into the gateway:
Verify the chain, with no
-k:Expect
SSL certificate verify ok, and a 308 from the http:// address.A browser shows a padlock and reports a secure context, which is what
WebAuthn and clipboard access need.
Verify the healthcheck:
Expect
healthywith SHELLHUB_AUTO_SSL on and off, and withSHELLHUB_PROXY on, since the listener wrappers apply to every server.
On master it is
unhealthywith TLS on, andhealthy-by-accident withTLS off:
wget http://gateway/anything-at-allreturns the same empty200 the probe was reading.
Half-set pair check: set only SHELLHUB_TLS_CERT_FILE and the gateway
refuses to start with a validation error, rather than silently asking
Let's Encrypt for a name it cannot prove.